package vd

import (
	"fmt"
	"sort"
	"errors"
	"strings"
	"strconv"
	"reflect"
	"math/big"
	"io/ioutil"
	"kumachan/interpreter/runtime/stdlib"
)


const GeneratedHeader = "// DO NOT EDIT: generated from Go program\n\n"

func MatchSource(expected string, file string) error {
	var src_bytes, err = ioutil.ReadFile(file)
	if err != nil {
		return err
	}
	var src = string(src_bytes)
	if strings.TrimRight(src, "\n") != strings.TrimRight(expected, "\n") {
		return errors.New(fmt.Sprintf("unexpected content in file %s", file))
	}
	return nil
}

func generateCorrespondingSource (
	records         [] interface {},
	determine_type  func(reflect.Type) *Type,
) string {
	var type2code func(*Type) string
	type2code = func(t *Type) string {
		switch t.kind {
		case Bool:    return stdlib.Bool
		case Float:   return stdlib.Float
		case Integer: return stdlib.Int
		case String:  return stdlib.String
		case Binary:  return stdlib.Bytes
		case Array:
			var el = type2code(t.elementType)
			return fmt.Sprintf("%s[%s]", stdlib.List, el)
		case Optional:
			var el = type2code(t.elementType)
			return fmt.Sprintf("%s[%s]", stdlib.Maybe, el)
		case Record, Enum:
			return t.identifier.Name
		default:
			panic("impossible branch")
		}
	}
	var dv2code = func(v reflect.Value) (string, bool) {
		// NOTE: the capability of this function is limited
		switch V := v.Interface().(type) {
		case bool:
			if V == true { return stdlib.Yes, true }
		case float64:
			if V != 0 { return fmt.Sprint(V), true }
		case complex128:
			if V != 0 { return fmt.Sprint(V), true }
		case *big.Int:
			if V.Cmp(big.NewInt(0)) != 0 { return fmt.Sprint(V), true }
		case string:
			if V != "" { return strconv.Quote(V), true }
		}
		return "", false
	}
	var curly_brace = func(s string) string {
		if s == "" {
			return "{}"
		} else {
			return fmt.Sprintf("{\n%s\n}", s)
		}
	}
	var enum_mapping = make(map[string] ([] string))
	var record_mapping = make(map[string] string)
	var is_empty_record = make(map[string] bool)
	for _, rec := range records {
		var struct_v = reflect.ValueOf(rec)
		var struct_t = struct_v.Type()
		var n = struct_t.NumField()
		var fields = make([] string, 0)
		var defaults = make([] string, 0)
		for i := 0; i < n; i += 1 {
			var name = struct_t.Field(i).Name
			var field_t = determine_type(struct_t.Field(i).Type)
			var t_code = type2code(field_t)
			var default_code = ""
			if dv_code, ok := dv2code(struct_v.Field(i)); ok {
				default_code = fmt.Sprintf(" default(%s)", dv_code)
			}
			fields = append(fields,
				fmt.Sprintf("\t%s %s%s", name, t_code, default_code),
			)
		}
		var fields_code = strings.Join(fields, ",\n")
		fields_code = curly_brace(fields_code)
		var defaults_code = strings.Join(defaults, ",\n")
		if defaults_code != "" {
			defaults_code = curly_brace(defaults_code)
			defaults_code = fmt.Sprintf("\ndefault %s", defaults_code)
		}
		// NOTE: should have used TypeId.Name but this is compatible now
		var rec_name = struct_t.Name()
		is_empty_record[rec_name] =
			struct_t.AssignableTo(reflect.TypeOf(struct {}{}))
		var rec_code =
			fmt.Sprintf("type %s open data record %s%s\n",
				rec_name,
				fields_code,
				defaults_code,
			)
		var m, exists = struct_t.MethodByName(EnumDummyInterfaceMethod)
		if exists {
			if m.Type.NumIn() != 2 { panic("invalid enum dummy method") }
			// NOTE: should have used TypeId.Name but this is compatible now
			var which = m.Type.In(1).Name()
			enum_mapping[which] = append(enum_mapping[which], rec_name)
		}
		record_mapping[rec_name] = rec_code
	}
	var enum_names = make([] string, 0, len(enum_mapping))
	var record_names = make([] string, 0, len(record_mapping))
	for k := range enum_mapping {
		enum_names = append(enum_names, k)
	}
	for k := range record_mapping {
		record_names = append(record_names, k)
	}
	sort.Strings(enum_names)
	sort.Strings(record_names)
	var buf strings.Builder
	buf.WriteString(GeneratedHeader)
	for _, k := range enum_names {
		buf.WriteString(fmt.Sprintf("type %s data union {\n", k))
		var items = enum_mapping[k]
		for i, rec_name := range items {
			buf.WriteString("\t")
			if is_empty_record[rec_name] {
				buf.WriteString("const ")
			}
			buf.WriteString(rec_name)
			if i != (len(items) - 1) {
				buf.WriteString(" |")
			}
			buf.WriteString("\n")
		}
		buf.WriteString("}\n")
	}
	for _, k := range record_names {
		buf.WriteString(record_mapping[k])
	}
	buf.WriteString("\n\n")
	return buf.String()
}


